<?php
// +----------------------------------------------------------------------
// | Authentication：身份认证
// | Authorization: 权限校验
// +----------------------------------------------------------------------

namespace helper;

use think\facade\Db;
use think\facade\Cookie;
use think\facade\Session;

/**
 * 用户认证
 */
class Auth
{
    /**
     * 用户表
     * @var array
     */
    private array $table;

    /**
     * 用户信息
     * @var mixed
     */
    private $user;

    /**
     * 令牌
     * @var string
     */
    private string $token = '';

    /**
     * Auth constructor.
     */
    public function __construct()
    {
        $this->setTable();
        $this->setUser();
    }

    /**
     * 设置AUTH表
     * @param array $table
     */
    public function setTable(array $table = [])
    {
        $this->table = [
            'user'             => $table['user'] ?? 'user',
            'role'             => $table['role'] ?? 'role',
            'permission'       => $table['permission'] ?? 'permission',
            'user_role'        => $table['user_role'] ??  'user_role',
            'role_permission'  => $table['role_permission'] ?? 'role_permission',
        ];
    }

    /**
     * 获得当前认证用户
     */
    public function user()
    {
        return $this->user;
    }

    /**
     * 获取当前认证用户ID
     * @return null|int
     */
    public function id(): ?int
    {
        if (!$this->user) {
            return null;
        }
        return $this->user->id;
    }

    /**
     * 记住用户
     * @param array $credentials
     * @param bool $remember
     * @return bool
     */
    public function attempt(array $credentials = [], bool $remember = false, $field = 'username'): bool
    {
        $user = $this->retrieveByCredentials($credentials, $field);
        if (!$user) {
            return false;
        }
        if (!$this->validateCredentials($user, $credentials)) {
            return false;
        }
        $this->login($user, $remember);
        return true;
    }

    /**
     * 如果要将已经存在的用户登入应用
     * @param array $user
     * @param false $remember
     */
    public function login(array $user, bool $remember = false)
    {
        if ($remember) {
            Cookie::forever('remember', 1);
        }
        $user['remember_token'] = $this->getToken([
            'iat' => time(),
            'exp' => time() + 30 * 24 * 60 * 60,
            'uid' => $user['id'],
            'sub' => $user['user_type']
        ]);
        $this->syncToken((object) $user, $user['remember_token']);
        $this->setUser($user);
    }

    /**
     * 设置用户
     * @param $user
     */
    private function setUser($user = null)
    {
        if ($user) {
            if (!$this->token) {
                Session::set('user', (object)$user);
            }
            $this->user = (object)$user;
        } else {
            if ($this->token) {
                $payload    = $this->getPayload();
                $user->id   = $payload['uid'];
                $this->user = $user;
            } else {
                $this->user = Session::get('user');
            }
        }
    }

    /**
     * 更新用户
     * @param array $user
     * @return array|null
     * @throws
     */
    public function syncUser(array $user = [])
    {
        $id = $this->id();
        if ($user) {
            $id = $user['id'] ?? $id;
            $user['id'] = $id;
            Db::name($this->table['user'])->update($user);
        }
        $user = $this->retrieveById($id);
        $this->setUser($user);
        return $user;
    }

    /**
     * 退出
     */
    public function logout(): bool
    {
        if ($this->viaRemember()) {
            $this->syncToken($this->user, '');
        }
        Session::delete('user');
        return true;
    }

    /**
     * 重置密码
     * @param array $credentials
     * @return int
     */
    public function password(array $credentials = [], $id = 0)
    {
        return $this->syncUser(['password' => $this->credentials($credentials), 'id' => $id ?? $this->id()]);
    }

    /**
     * 检查用户是否登录
     * @return boolean
     */
    public function check(): bool
    {
        return isset($this->user);
    }

    /**
     * 记住用户
     * @return boolean
     */
    public function viaRemember(): bool
    {
        return (bool)Cookie::get('remember');
    }

    /**
     * 查询用户
     * @param int $identifier
     * @return array|null
     * @throws
     */
    public function retrieveById(int $identifier): ?array
    {
        return Db::name($this->table['user'])->find($identifier);
    }

    /**
     * 查询用户
     * @param array $credentials
     * @param string $field
     */
    public function retrieveByCredentials(array $credentials, string $field)
    {
        if (empty($credentials['password'])) {
            return null;
        }
        return Db::name($this->table['user'])->where($field, $credentials['username'])->find();
    }

    /**
     * 密码校验
     * @param $user
     * @param array $credentials
     * @return bool
     */
    public function validateCredentials($user, array $credentials): bool
    {
        return password_verify($credentials['password'], $user['password']);
    }

    /**
     * 密码加密
     * @param array $credentials
     * @return false|string|null
     */
    public function credentials(array $credentials)
    {
        return password_hash($credentials['password'], PASSWORD_DEFAULT);
    }

    /**
     * 设置appId
     * @param string $pre
     * @return array
     */
    public function setApp(string $pre = 'wx'): array
    {
        $str       = md5(date('YmdHis'));
        $pre       = $pre . substr($str, 1, 3);
        $appId     = uniqid($pre);
        $appSecret = md5(sha1($appId));
        return compact('appId', 'appSecret');
    }

    /**
     * 获取appId
     * @return array
     */
    public function getApp(): array
    {
        return ['appId' => $this->user->app_id, 'appSecret' => $this->user->app_secret];
    }

    /**
     * 设置令牌
     * @param string $authorization
     * @return Auth
     */
    public function setToken(string $authorization): Auth
    {
        // Basic 账号:密码
        // Bearer token
        if (strpos($authorization, 'Bearer') !== 0) {
            $token = '';
        } else {
            $token = substr($authorization, 7);
        }
        $this->token = $token;
        return $this;
    }

    /**
     * 获取jwt token https://jwt.io/
     * @param array $payload 载荷
     * [
     *  'iss'=>'签发者',
     *  'iat'=>'签发时间',
     *  'exp'=>'过期时间',
     *  'sub'=>'面向的用户',
     *  'aud'=>'接收的一方',
     *  'nbf'=>'某个时间点后才能访问',
     *  'jti'=>'唯一标识'
     * ]
     * @return string
     */
    public function getToken(array $payload): string
    {
        // 头部
        $header        = [
            'alg' => 'HS256', //生成signature的算法
            'typ' => 'JWT'    //类型
        ];
        $base64header  = $this->base64UrlEncode(json_encode($header, JSON_UNESCAPED_UNICODE));
        $base64payload = $this->base64UrlEncode(json_encode($payload, JSON_UNESCAPED_UNICODE));
        $signature     = $this->signature($base64header . '.' . $base64payload, $payload['sub'] ?? 'JWT');
        return $base64header . '.' . $base64payload . '.' . $signature;
    }

    /**
     * 获取用户信息
     * @return array
     */
    public function getPayload(): ?array
    {
        $tokens = explode('.', $this->token);
        if (count($tokens) != 3) {
            return ['uid' => 0, 'sub' => 0];
        }
        return json_decode($this->base64UrlDecode($tokens[1]), JSON_OBJECT_AS_ARRAY);
    }

    /**
     * 更新令牌
     * @param object $user
     * @param string $token
     * @return array
     * @throws
     */
    public function syncToken(object $user, string $token): array
    {
        return $this->syncUser(['remember_token' => $token, 'id' => $user->id]);
    }

    /**
     * 验证token是否有效
     * @return bool
     */
    public function tokenVerify(): bool
    {
        $tokens = explode('.', $this->token);
        if (count($tokens) != 3) {
            return false;
        }

        list($base64header, $base64payload, $signature) = $tokens;

        $header = json_decode($this->base64UrlDecode($base64header), JSON_OBJECT_AS_ARRAY);
        if (empty($header['alg'])) {
            return false;
        }

        $payload = json_decode($this->base64UrlDecode($base64payload), JSON_OBJECT_AS_ARRAY);

        // 签发时间
        if (isset($payload['iat']) && $payload['iat'] > time()) {
            return false;
        }

        // 过期时间
        if (isset($payload['exp']) && $payload['exp'] < time()) {
            return false;
        }

        // 禁用时间
        if (isset($payload['nbf']) && $payload['nbf'] > time()) {
            return false;
        }

        // 签名验证
        if ($this->signature($base64header . '.' . $base64payload, $payload['sub']) !== $signature) {
            return false;
        }
        return true;
    }

    /**
     * base64UrlEncode
     * @param string $input 需要编码的数据
     * @return string
     */
    private function base64UrlEncode(string $input): string
    {
        return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
    }

    /**
     * base64UrlEncode
     * @param string $input 需要解码的字符串
     * @return bool|string
     */
    private function base64UrlDecode(string $input)
    {
        $remainder = strlen($input) % 4;
        if ($remainder) {
            $len   = 4 - $remainder;
            $input .= str_repeat('=', $len);
        }
        return base64_decode(strtr($input, '-_', '+/'));
    }

    /**
     * HMAC SHA256签名
     * @param string $data 为base64UrlEncode(header).".".base64UrlEncode(payload)
     * @param string $key 密钥
     * @return string
     */
    private function signature(string $data, string $key): string
    {
        return $this->base64UrlEncode(hash_hmac('sha256', $data, $key, true));
    }

    /**
     * 获取用户角色
     * @param int $uid
     * @return array|null
     */
    public function getRole(int $uid)
    {
        static $role = [];
        if (isset($role[$uid])) {
            return $role[$uid];
        }
        $roles = Db::name($this->table['role'])
            ->field('r.id as role_id,r.name as role_name,r.remark')
            ->alias('r')
            ->join([$this->table['user_role'] => 'u'], 'r.id=u.role_id')
            ->where('user_id', $uid)
            ->select();
        $role[$uid] = $roles ?: [];
        return $role[$uid];
    }

    /**
     * 给用户分配角色
     * @param array $param
     * @return int|string
     */
    public function assignRole(array $param)
    {
        $data = ['user_id' => $param['user_id'], 'role_id' => $param['role_id']];
        return Db::name($this->table['user_role'])->insert($data);
    }

    /**
     * 更新用户角色
     * @param array $param
     * @return int|string
     */
    public function syncRole(array $param)
    {
        $data = ['user_id' => $param['user_id'], 'role_id' => $param['role_id']];
        return Db::name($this->table['user_role'])->save($data);
    }

    /**
     * 判断用户角色
     * @param int $role_id
     * @return array|null
     */
    public function hasRole(int $role_id)
    {
        return Db::name($this->table['user_role'])->find($role_id);
    }

    /**
     * 删除用户角色
     * @param int $role_id
     * @return int
     */
    public function removeRole(int $role_id)
    {
        return Db::name($this->table['user_role'])->delete($role_id);
    }

    /**
     * 检查权限
     * @param string|array $permission
     * @param int $uid
     * @param string $relation 如果为 'or' 表示满足任一条规则即通过验证;如果为 'and'则表示需满足所有规则才能通过验证
     * @return bool
     */
    public function hasPermission($permission, int $uid = 0, string $relation = 'or'): bool
    {
        // 获取用户需要验证的所有有效规则列表
        $authList = $this->getPermission($uid);
        if (is_string($permission)) {
            $permission = strtolower($permission);
            if (strpos($permission, ',') !== false) {
                $permission = explode(',', $permission);
            } else {
                $permission = [$permission];
            }
        }
        $list    = [];//保存验证通过的规则名
        $REQUEST = unserialize(strtolower(serialize($_REQUEST)));
        foreach ($authList as $auth) {
            $query = preg_replace('/^.+\?/U', '', $auth);
            if ($query != $auth) {
                // 解析规则中的param
                parse_str($query, $param);
                $intersect = array_intersect_assoc($REQUEST, $param);
                $auth      = preg_replace('/\?.*$/U', '', $auth);
                // 如果节点相符且url参数满足
                if (in_array($auth, $permission) && $intersect == $param) {
                    $list[] = $auth;
                }
            } elseif (in_array($auth, $permission)) {
                $list[] = $auth;
            }
        }
        if ($relation === 'or' && !empty($list)) {
            return true;
        }
        $diff = array_diff($permission, $list);
        if ($relation === 'and' && empty($diff)) {
            return true;
        }
        return false;
    }

    /**
     * 获取用户组权限
     * @param int $uid
     * @return array|mixed
     */
    public function getPermission(int $uid)
    {
        // 保存用户验证通过的权限列表
        static $permission = [];
        if (isset($permission[$uid])) {
            return $permission[$uid];
        }
        //if (Session::has('_AUTH_LIST_' . $uid)) {
            //return Session::get('_AUTH_LIST_' . $uid);
        //}
        // 读取用户所属用户组
        $roles = $this->getRole($uid);
        // 保存用户所属用户组设置的所有权限规则ID
        $ids = [];
        foreach ($roles as $r) {
            $ids = array_merge($ids, explode(',', trim($r['role_id'], ',')));
        }
        $ids = array_unique($ids);
        if (empty($ids)) {
            $permission[$uid] = [];
            return [];
        }
        $map = [
            ['role_id', 'in', $ids],
        ];
        // 读取用户组所有权限规则
        $rules = Db::name($this->table['role_permission'])
            ->alias('rp')
            ->join($this->table['permission'] . ' p', 'rp.permission_id=p.id')
            ->where($map)
            ->column('route');

        // 循环规则
        $permissions = [];
        foreach ($rules as $rule) {
            $permissions[] = strtolower($rule);
        }
        $permission[$uid] = array_unique($permissions);
        //Session::set('_AUTH_LIST_' . $uid, $permission[$uid]);
        return $permission[$uid];
    }
}